 
#这是一个下载m3u8 视频资源的脚本   无指定序号版，根据资源数组排序 非ffmpeg合并版
import subprocess
import os
import re
import sys
import m3u8 
import glob
import time
import requests
import concurrent.futures
from Crypto.Cipher import AES
from concurrent.futures import as_completed
 
 
#请求头
headers = { 
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'Accept-Language': 'Zh-CN, zh;q=0.9, en-gb;q=0.8, en;q=0.7'
} 
 
 
#判断是否为网站地址
def reurl(url):
    pattern = re.compile(r'^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+')
    m=pattern.search(url)
    if m is None:
        return False
    else:
        return True
 
#获取密钥（针对有些m3u8文件中的视频需要key去解密下载的视频）
def getKey(keystr,url):
    keyinfo= str(keystr)
    method_pos= keyinfo.find('METHOD')
    comma_pos = keyinfo.find(",")
    method = keyinfo[method_pos:comma_pos].split('=')[1]
    uri_pos = keyinfo.find("URI")
    quotation_mark_pos = keyinfo.rfind('"')
    key_url = keyinfo[uri_pos:quotation_mark_pos].split('"')[1]
    if reurl(key_url) == False:
        key_url = url.rsplit("/", 1)[0] + "/" + key_url
    res = requests.get(key_url,headers=headers)
    key = res.content
    print(method)
    print(key.decode('utf-8','ignore'))
    return method, key
 
#下载文件
#down_url:ts文件地址
#url:*.m3u8文件地址
#decrypt:是否加密
#down_path:下载地址
#key:密钥
def download(down_url,url,decrypt,down_path,key,nameid):
    if reurl(down_url) == False:
        if len(down_url.rsplit("/", 1))>1:
            filename = down_url.rsplit("/", 1)[1]
        else:
            filename = down_url
        down_url = url.rsplit("/", 1)[0] + "/" + down_url
    else:
        filename = down_url.rsplit("/", 1)[1]
    down_ts_path = down_path+"/{0}".format(filename)
    if os.path.exists(down_ts_path):
        print('文件 '+filename+' 已经存在，跳过下载.')
    else: 
        try:
            res = requests.get(down_url, stream=True, verify=False,headers=headers)
            print('正在下载资源：'+filename+'')
        except Exception as e:
            print('requests error:',e)
            return
        if decrypt:
            cryptor =  AES.new(key, AES.MODE_CBC, key)
        
        with open(down_ts_path,"wb+") as file:
            for chunk in res.iter_content(chunk_size=1024):
                if chunk:
                    if decrypt:
                        file.write(cryptor.decrypt(chunk))
                    else:
                        file.write(chunk)
            print('文件:['+filename+']已保存到['+down_path+']目录.')
    
#合并ts文件
#dest_file:合成文件名
#source_path:ts文件目录
#ts_list:文件列表
#delete:合成结束是否删除ts文件   
def merge_to_mp4(dest_file, source_path,ts_list, delete=False):
    # files = glob.glob(source_path + '/*.m4s')
    # files = glob.glob(source_path + '/*.ts')
    # print('ts_list', len(files), len(ts_list))
    # if len(files)!=len(ts_list):
    #     print("文件不完整，已取消合并！请重新执行一次脚本，完成未下载的文件。\n如果确认已下载完所有文件，请检查下载目录移除其它无关的ts文件。")
    #     return
    print('开始合并['+source_path+']目录的ts视频...')
 
    # with open(dest_file, 'wb') as fw:
    #     for file in ts_list:
    #         with open(source_path+"/"+file, 'rb') as fr:
    #             fw.write(fr.read())
    #         if delete:
    #             os.remove(file)
    #     print('合并完成！ 文件名：'+dest_file+'')

    with open('input.txt', 'w') as f:  
      for ts in ts_list:  
        # 构建每个文件的绝对路径并写入到input.txt中  
        abs_ts_path = os.path.join(source_path, ts)  
        abs_path = os.path.abspath(abs_ts_path)  
        f.write("file '{}'\n".format(abs_path))  

  
    # 调用ffmpeg进行合并  
    command = [  
        'ffmpeg',  
        '-f', 'concat',  
        '-safe', '0',  
        '-i', 'input.txt',  
        '-c', 'copy',  # 如果所有片段都有相同的编解码器和参数，可以使用copy来避免重新编码  
        # 如果需要转码或参数不一致，可以移除'-c copy'并使用'-c:v libx264'等指定编解码器  
        dest_file  
    ]  
    subprocess.run(command, check=True) 
 
def main():    
 
    url = "https://video.twimg.com/ext_tw_video/1791441441673351169/pu/pl/avc1/1280x720/F4w4b-MR4CVODD1h.m3u8" #下载地址,通过 cmd 传入或输入
 
    print('\n')
    print('参数说明:脚本后面面添加 m3u8地址参数，如打开CMD(终端命令)模式输入：m3u8dl http://xxx.xxx.com/xxx.m3u8')
    print('\n')
    print('    如果m3u8地址访问不到，提示错误，多重复几次就好。前提是确认在线能观看可下载到m3u8文件。')
    print('    下载中途不动了或者关机，可关闭取消下载，再次打开继续下载。')
    print('    有些文件一次下载不到，需要多次执行下载。')
    print('    等所有文件下载完后自动合成一个视频，注意看提示。')
    print('\n')
 
    if len(sys.argv)>1:
        url=(sys.argv[1])
    else:
        print('亲，没有添加m3u8地址,请在下方输入:')
        url=input()
 
    #禁止安全谁提示信息
    requests.packages.urllib3.disable_warnings() 
 
 
    print('开始分析m3u8文件资源...')
    #使用m3u8库获取文件信息    
    try:
        video = m3u8.load(url, timeout=20, headers=headers)
    except Exception as e:
        print('m3u8文件资源连接失败！请检查m3u8文件地址并重试.错误代码:',e)
        return
 
    #设置下载路径
    down_path="tmp"
    #设置是否加密标志
    decrypt = False
    #ts列表
    ts_list=[]
    #判断是否加密
    key=''
    if video.keys[0]  is not None:
        method,key =getKey(video.keys[0],url)
        decrypt = True
     #判断是否需要创建文件夹
    if not os.path.exists(down_path):
        os.mkdir(down_path)
    #把ts文件名添加到列表中
    for filename  in video.segments:
        if len(filename.uri.rsplit("/", 1))>1:
            ts_list.append(filename.uri.rsplit("/", 1)[1])
        else:
            ts_list.append(filename.uri)        
    #开启线程池
    with concurrent.futures.ThreadPoolExecutor() as executor:
        obj_list = []
        begin = time.time()#记录线程开始时间
        for i in range(len(video.segments)):
            obj = executor.submit(download,video.segments[i].uri,url,decrypt,down_path,key,i)
            obj_list.append(obj)
        #查看线程池是否结束
        for future in as_completed(obj_list):
            data = future.result()
            # print('completed result:',data)
        merge_to_mp4('finalvideo.mp4', './' + down_path,ts_list)#合并ts文件
        times = time.time() - begin #记录线程完成时间
        print('总消耗时间:'+str(times)+'')
 
if __name__ == "__main__":
    main()



# python3 -m pip install xx   使用python3下对应的pip安装依赖
# python3 -m pip install --upgrade pip 升级python3下对应的pip
# ffmpeg -i url.m3u8 -bsf:a aac_adtstoasc -c copy out2.mp4 -http_persistent 0 // 

# 汇总 -codec
# -acodec copy = -c:a copy 表示复制音频流而不重新编码。还可以这样写-acodec:copy  -c:a aac表示将音频流编码为AAC格式
# -c:s copy 来复制字幕流
# -vcodec copy = -c:v copy表示复制视频流而不重新编码  -vcodec:copy
# -c copy = -codec copy 表示复制所有信息不重新编码 -codec:copy


# m3u8转音频
# -vn选项表示“无视频”，
# ffmpeg -i url.m3u8 -vn -acodec copy audio_only.aac
# 抽取音频流：ffmpeg -i url.mp4 -vn -c:a copy audio_only.aac
# 抽取视频流： ffmpeg -i url.mp4 -an -c:v copy video_only.mp4 / video_only.h264
# 转视频格式： ffmpeg -i url.mp4 -c:v copy -c:a copy video.flv 
# 或者不区分音频视频，直接-c针对所有流：ffmpeg -i url.mp4 -c copy video.flv

# -frames:v 是 FFmpeg 命令中用于设置输出视频帧数的参数的简写。它与 -vframes 功能相同，都是设置输出的视频帧数
# 截取视频中的一帧图： ffmpeg -i url.mp4 -ss 00:00:50 -frames:v 1 out.jpg

# m3u8转视频  -bsf:a: bsf 表示 binary stream filter，即二进制流过滤器，a 表示 audio，指定音频比特流过滤器 aac_adtstoasc: 这是一个特定的比特流过滤器，用于将 AAC 音频从 ADTS 格式转换为 ASC 格式
# -http_persistent 0: 禁用 HTTP 持久连接。默认情况下，ffmpeg 可能会尝试使用持久连接来减少重新建立连接的开销。但在某些情况下，特别是当服务器不支持或限制持久连接时，禁用它可能是有益的
# ffmpeg -i url.m3u8 -bsf:a aac_adtstoasc -c copy out2.mp4 -http_persistent 0
# 简单一些： ffmpeg -i url.m3u8 -c copy out2.mp4


# 合并音频和视频 
# -strict experimental在某些版本的ffmpeg中可能是必要的，用于启用AAC编码。-map选项用来指定要包含到输出文件中的流
# ffmpeg -i out2.mp4 -i audio_only.aac -c:v copy -c:a aac -strict experimental -map 0:v:0 -map 1:a:0 output_video_with_audio.mp4
# or 本身音频就是aac的，无需编码直接copy
# ffmpeg -i out2.mp4 -i audio_only.aac -c:v copy -c:a copy output_video_with_audio.mp4
# 简单一些： ffmpeg -i out2.mp4 -i audio_only.aac -c copy output_video_with_audio.mp4

# 0:v:0：选择第一个输入文件（由 -i video_without_audio.mp4 指定）中的第一个视频流（索引为0）。这里的 0 表示输入文件的索引（从0开始），v 表示视频流，0 表示视频流的索引。
# 1:a:0：选择第二个输入文件（由 -i audio_only.aac 指定）中的第一个音频流（索引为0）。这里的 1 表示第二个输入文件的索引，a 表示音频流，0 表示音频流的索引。
# 通过这两个 -map 选项，你确保了在输出文件 output_video_with_audio.mp4 中只包含了第一个输入文件的视频流和第二个输入文件的音频流。


# 添加水印：ffmpeg -i out2.mp4 -i out.jpg -filter_complex "overlay=main_w-overlay_w-10:main_h-overlay_h-10" output_with_watermark.mp4
# -filter_complex 用于指定复杂的滤镜链。verlay=main_w-overlay_w-10:main_h-overlay_h-10 是 overlay 滤镜的参数，用于指定水印的位置。这里，水印被放置在距离视频右边和底部各 10 像素的位置
# overlay=10:10 会将水印放置在视频的左上角: ffmpeg -i out2.mp4 -i out.jpg -filter_complex "overlay=10:10" output_with_watermark.mp4

# 进阶：[1:v]选择第二个文件流，也就是out.jpg，scale=64:48[watermark]，scale滤镜将上述的图片设置比例，然后取名为watermark，方便后面用。
# [0:v]选择第一个视频流，[watermark]overlay=30:10选择上述设置尺寸的watermark，使用overlay 滤镜放在主视频的左边30，上边10的位置
# [out] 给输出流取个名字为out。 -map "[out]" 是在有多个输出进行选择时有用，下面比较多余。
#设置图片大小：ffmpeg -i out2.mp4 -i out.jpg -filter_complex "[1:v]scale=64:48[watermark];[0:v][watermark]overlay=30:10[out]" -map "[out]" output.mp4
# 新版用filter_complex滤镜，旧版是movie滤镜：ffmpeg -i input.mp4 -vf "movie=logo.png,scale=64:48[watermask];[in][watermask] overlay=30:10 [out]" output.mp4

# -vf 用于指定一个或多个视频滤镜，并通过一个冒号分隔的列表来组织它们；delogo滤镜，xy是水印位置， wh是水印尺寸
# 删除水印：ffmpeg -i output.mp4 -vf "delogo=x=30:y=10:w=64:h=48" output_without_logo.mp4  
# 我觉得想知道视频水印在哪里，可以先截取一帧，再到图片上量一量？

# 合并多段视频：ffmpeg -f concat -i inputs.txt -c copy out.mp4  // txt文件里就是多段视频，格式为：file '1.mp4'

# -map 0：选择所有输入流，-f segment：指定输出格式为segment，这将生成多个片段文件。
# -segment_list playlist.m3u8： 指定文件名， -segment_time 10：设置每个片段的时长为10秒
# output%03d.ts：输出片段文件的模板。%03d是一个占位符，用于自动插入从000开始的递增数字
# ts切片成m3u8: ffmpeg -i input.ts -codec copy -map 0 -f segment -segment_list playlist.m3u8 -segment_time 10 output%03d.ts
# 也可以直接将mp4 转为m3u8: ffmpeg -i input.mp4 -codec:copy -map 0 -f segment -segment_list playlist.m3u8 -segment_time 10 output%03d.ts
# 当然也可以先将mp4 转为ts 文件：ffmpeg -i input.mp4 -c copy -bsf:v h264_mp4toannexb output.ts